package top.lingkang.finalgateway.core.http;

import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.StrUtil;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.*;
import io.netty.handler.codec.http.multipart.*;
import io.netty.handler.stream.ChunkedFile;
import lombok.extern.slf4j.Slf4j;
import okhttp3.*;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import top.lingkang.finalgateway.config.NextTarget;
import top.lingkang.finalgateway.config.RouterConfig;
import top.lingkang.finalgateway.config.RouterType;
import top.lingkang.finalgateway.core.RequestHandler;
import top.lingkang.finalgateway.core.StaticFile;
import top.lingkang.finalgateway.utils.CommonUtils;
import top.lingkang.finalgateway.utils.HttpUtils;
import top.lingkang.finalgateway.utils.StaticMimes;

import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;

/**
 * @author lingkang
 * Created by 2024/5/5
 */
@Slf4j
public class HttpHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
    private static final Logger access = LoggerFactory.getLogger("access");
    private static final MediaType JSON_MediaType = MediaType.get("application/json; charset=utf-8");
    private final RouterConfig config;
    private FullHttpRequest request;
    private final String ip;
    private long total = 0;
    private String target;
    private String type;

    public HttpHandler(RouterConfig config, String ip) {
        this.config = config;
        this.ip = ip;
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest msg) throws Exception {
        this.request = msg;
        // 访问日志
        access.info("http ip={} method={} url={} User-Agent={}",
                ip, request.method().name(), request.uri(), request.headers().get("User-Agent"));

        // 请求处理
        RequestHandler handler = config.getHandlerMap().get(request.uri());
        if (handler == null) {// cache
            handler = config.convertHttp(request.uri());
            config.getHandlerMap().put(request.uri(), handler);
        }

        if (handler.getType() == RouterType.PROXY_PASS) {
            remoteHandler(handler, ctx);
            type = "http";
            return;
        } else if (handler.getType() == RouterType.DIST) {
            handlerStaticFile(handler, ctx);
            type = "dist";
            return;
        }

        HttpUtils.return404Json(ctx);
        log.info("404 path={}", request.uri());
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        log.error("error, port={} {} - {}", config.getPort(), request.method().name(), request.uri(), cause);
        if (ctx.channel().isActive())
            HttpUtils.return505Json(ctx);
    }

    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
        super.handlerRemoved(ctx);
        config.getMonitor().add(
                type,
                ip,
                request.method().name(),
                request.uri(),
                target,
                total
        );
    }


    public void remoteHandler(RequestHandler handler, ChannelHandlerContext context) throws IOException {
        NextTarget next = config.nextTarget(handler);
        target = next.getTarget();
        pass(next, request, context);
    }

    private void pass(NextTarget next, FullHttpRequest request, ChannelHandlerContext context) throws IOException {
        Request.Builder builder = new Request.Builder()
                .url(next.getTarget());
        String referer = request.headers().get("referer");
        if (referer != null) {
            String urlHost = request.headers().get("host");
            if (urlHost == null) {
                request.headers().remove("referer");
            } else {
                request.headers().set("referer", referer.replace(urlHost, next.getHost()));
            }
        }
        request.headers().set("host", next.getHost());
        request.headers().remove("origin");
        request.headers().forEach(e -> {
            builder.header(e.getKey(), e.getValue());
        });
        if (request.method() != HttpMethod.GET) {
            FormBody.Builder formBody = new FormBody.Builder();
            HttpPostRequestDecoder postRequestDecoder = new HttpPostRequestDecoder(config.getHttpDataFactory(), request);
            List<InterfaceHttpData> bodyHttpDatas = postRequestDecoder.getBodyHttpDatas();
            if (!bodyHttpDatas.isEmpty()) {
                String contentType = request.headers().get("content-type");
                if (contentType != null && contentType.toLowerCase().contains("application/json")) {
                    if (request.method() == HttpMethod.POST)
                        builder.post(RequestBody.create(bodyHttpDatas.get(0).getName(), JSON_MediaType));
                    else if (request.method() == HttpMethod.PUT)
                        builder.put(RequestBody.create(bodyHttpDatas.get(0).getName(), JSON_MediaType));
                    else if (request.method() == HttpMethod.DELETE)
                        builder.delete(RequestBody.create(bodyHttpDatas.get(0).getName(), JSON_MediaType));
                    else {
                        log.warn("不支持的请求类型: {}", request.method().name());
                        HttpUtils.returnNotSupported(context);
                        return;
                    }
                } else {
                    MultipartBody.Builder multipartBody = null;
                    for (InterfaceHttpData param : bodyHttpDatas) {
                        if (param.getHttpDataType() == InterfaceHttpData.HttpDataType.Attribute) {
                            if (param instanceof MemoryAttribute) {
                                MemoryAttribute data = (MemoryAttribute) param;
                                formBody.add(param.getName(), data.getValue());
                            } else if (param instanceof MixedAttribute) {
                                MixedAttribute data = (MixedAttribute) param;
                                formBody.add(param.getName(), data.getValue());
                            } else {
                                log.warn("未识别的类别：{} --> {}", param.getClass(), param);
                            }
                        } else if (param.getHttpDataType() == InterfaceHttpData.HttpDataType.FileUpload) {
                            FileUpload data = (FileUpload) param;
                            log.debug("{} 上传文件大小：size={}, 名称：{}", data.getName(), data.length(), data.getFilename());
                            if (multipartBody == null)
                                multipartBody = new MultipartBody.Builder();
                            String mime = StaticMimes.get("." + FileUtil.getSuffix(data.getFilename()));
                            if (data.isInMemory()) {// 内存中的文件
                                multipartBody.addFormDataPart(
                                        data.getName(),
                                        data.getFilename(),
                                        RequestBody.create(data.get(), MediaType.parse(mime))
                                );
                            } else {
                                multipartBody.addFormDataPart(
                                        data.getName(),
                                        data.getFilename(),
                                        RequestBody.create(data.getFile(), MediaType.parse(mime))
                                );
                            }
                        }
                    }
                    if (multipartBody != null) {
                        try {
                            ArrayList<String> names = (ArrayList<String>) CommonUtils.FormBody_names.get(formBody);
                            ArrayList<String> values = (ArrayList<String>) CommonUtils.FormBody_values.get(formBody);
                            for (int i = 0; i < names.size(); i++) {
                                multipartBody.addFormDataPart(names.get(i), values.get(i));
                            }
                        } catch (Exception e) {
                            log.warn("处理multipartBody参数异常", e);
                        }
                        builder.method(request.method().name(), multipartBody.build());
                    } else {
                        // post \delete  \ put
                        builder.method(request.method().name(), formBody.build());
                    }
                }
            } else {
                // 发送空数据时
                builder.method(request.method().name(), formBody.build());
            }
        } else {
            // GET
        }
        config.getClient().newCall(builder.build()).enqueue(new Callback() {
            @Override
            public void onFailure(@NotNull Call call, @NotNull IOException e) {
                log.warn("remote error, port={} method={} source={}  target={}\n{}",
                        config.getPort(), request.method().name(), request.uri(), next.getTarget(), e.getMessage());
                String msg = "{\"code\":1,\"msg\":\"" + e.getMessage() + "\"}";
                FullHttpResponse result = new DefaultFullHttpResponse(
                        HttpVersion.HTTP_1_1, HttpResponseStatus.valueOf(500),
                        Unpooled.wrappedBuffer(msg.getBytes(StandardCharsets.UTF_8)), new DefaultHttpHeaders(), DefaultHttpHeadersFactory.headersFactory().newHeaders()
                );
                context.writeAndFlush(result).addListener(ChannelFutureListener.CLOSE);
            }

            @Override
            public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
                HttpHeaders headers = new DefaultHttpHeaders();
                response.headers().forEach(pair -> {
                    headers.set(pair.getFirst(), pair.getSecond());
                });

                ResponseBody body = response.body();
                if (body == null) {
                    FullHttpResponse result = new DefaultFullHttpResponse(
                            HttpVersion.HTTP_1_1, HttpResponseStatus.valueOf(response.code()),
                            Unpooled.EMPTY_BUFFER, headers, DefaultHttpHeadersFactory.headersFactory().newHeaders()
                    );
                    context.writeAndFlush(result).addListener(ChannelFutureListener.CLOSE);
                } else {
                    DefaultHttpResponse res = new DefaultHttpResponse(
                            HttpVersion.HTTP_1_1,
                            HttpResponseStatus.valueOf(response.code()),
                            headers
                    );
                    context.writeAndFlush(res);

                    InputStream in = body.source().inputStream();
                    byte[] bytes = new byte[32 * 1024];
                    int len;
                    while ((len = in.read(bytes)) != -1) {
                        context.writeAndFlush(new DefaultHttpContent(Unpooled.copiedBuffer(bytes, 0, len)));
                        total += len;
                    }
                    // 数据发送完成
                    context.writeAndFlush(new DefaultLastHttpContent()).addListener(ChannelFutureListener.CLOSE);
                    // 关闭io
                    in.close();
                }
                response.close();
            }
        });
    }

    // 静态文件处理
    private void handlerStaticFile(RequestHandler handler, ChannelHandlerContext ctx) throws Exception {
        StaticFile file = handler.getFile();
        if (file == null) {
            HttpUtils.return404Json(ctx);
            return;
        }
        HttpHeaders headers = new DefaultHttpHeaders();
        headers.set(HttpHeaderNames.ACCEPT_RANGES, HttpHeaderValues.BYTES);
        headers.set(HttpHeaderNames.CONTENT_LENGTH, file.length());
        headers.set(HttpHeaderNames.LAST_MODIFIED, file.lastModified());
        // 设置文件请求头
        String ct = StaticMimes.mimeMap.get(file.getSuffix());
        if (ct != null)
            headers.set(HttpHeaderNames.CONTENT_TYPE, ct);

        // 304缓存  If-Modified-Since
        if (handler.isFileCache()) {
            headers.set(HttpHeaderNames.CACHE_CONTROL, HttpHeaderValues.PUBLIC);// Cache-Control: public
            String timeMillis = request.headers().get(HttpHeaderNames.IF_MODIFIED_SINCE);
            if (String.valueOf(file.lastModified()).equals(timeMillis)) {
                DefaultHttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.NOT_MODIFIED, headers);
                ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
                return;
            }
        }

        long offset = 0L, length = file.length();
        HttpResponseStatus status = HttpResponseStatus.OK;

        /**
         * 需要断点续传协议: Content-Range: <unit> <range-start>-<range-end>/<size> 其中size是指整个文件的大小
         * Content-Range: https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Content-Range
         * Range: https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Range
         */
        String range = request.headers().get(HttpHeaderNames.RANGE);
        if (StrUtil.isNotBlank(range)) {// Range: bytes=1900544-  Range: bytes=1900544-6666666
            range = range.substring(6);
            String[] split = range.split("-");
            try {
                offset = Long.parseLong(split[0]);
                if (offset >= length) {
                    // 返回
                    HttpUtils.sendString("服务器无法处理所请求的数据区间", 416, ctx);
                    return;
                }
                if (split.length == 2) {
                    long end = Long.parseLong(split[1]);
                    if (end < length) {
                        headers.set(HttpHeaderNames.CONTENT_RANGE, "bytes " + range + "/" + file.length());
                        length = end - offset + 1;
                    } else {
                        // 返回
                        HttpUtils.sendString("服务器无法处理所请求的数据区间", 416, ctx);
                        return;
                    }
                } else {
                    headers.set(HttpHeaderNames.CONTENT_RANGE, "bytes " + offset + "-" + (length - 1) + "/" + file.length());
                    length = length - offset;
                }
                headers.set(HttpHeaderNames.CONTENT_LENGTH, length);// 重写响应长度
                status = HttpResponseStatus.PARTIAL_CONTENT; // 206
            } catch (Exception e) {
                log.warn("断点续传解析错误", e);
                // 返回 https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Status/416
                HttpUtils.sendString("服务器无法处理所请求的数据区间", 416, ctx);
                return;
            }
        }

        DefaultHttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, status, headers);

        ctx.writeAndFlush(response);
        RandomAccessFile accessFile = new RandomAccessFile(file, "r");
        ctx.writeAndFlush(
                //new DefaultFileRegion(file, offset, length), // 零拷贝，部分系统可能不支持，低版本jdk6以下存在bug
                new ChunkedFile(
                        accessFile, offset, length, 65536), // 正常发送文件块
                ctx.newProgressivePromise()
        ).addListener(ChannelFutureListener.CLOSE);
        total = length;
    }

    private void closeOnFlush(ChannelHandlerContext ctx) {
        if (ctx.channel().isActive()) {
            ctx.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
        }
    }
}
